package com.inspectortime.webapp.inspector.fee;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import com.inspectortime.domain.pricing.BaseFeeWithAdjustersCalculator;
import com.inspectortime.domain.pricing.FeeCalculator;
import com.inspectortime.domain.type.FoundationType;
import com.inspectortime.domain.type.PropertyType;
import com.mkode.util.java.lang.ClassUtils;

/**
 * @author mkvalsvik
 * 
 */
public class FeeCalculatorFormValidator implements Validator {

	public boolean supports(Class clazz) {
		return ClassUtils.isInstanceOf(clazz, FeeCalculator.class);
	}

	private static Log log = LogFactory
			.getLog(FeeCalculatorFormValidator.class);

	/*
	 * // So the basic rule is this: // Reject current calculator if it supports
	 * the sampleInspection (generated by another calculator) // ... UNLESS the
	 * current calculator specifies a zip code AND the other one does not // ...
	 * OR the current calculator does not specify a zip while the other one does
	 * // In summary: We allow overlapping calculators as long as one specifies
	 * a zip and the other doesn't. The reason for this is that when calculating
	 * price we will consult the zip-specific calculators first, and only
	 * consult general (no zip) calculators if a zip-specific one matches.
	 */
	private void validateAgainstOverlapWithEachOther(
			FeeCalculator currentCalculator, FeeCalculator otherCalculator,
			Errors errors) {

		// If one has zips and the other doesn't, then there's no need to
		// validate as the one with zips will be chosen first if there is
		// overlap
		if (currentCalculator.hasSupportedZipCodes()
				&& !otherCalculator.hasSupportedZipCodes()
				|| !currentCalculator.hasSupportedZipCodes()
				&& otherCalculator.hasSupportedZipCodes()) {
			return;
		}

		// zips are specified but different
		if (currentCalculator.hasSupportedZipCodes()
				&& otherCalculator.hasSupportedZipCodes()) {
			if (!otherCalculator.isSupportedZip(currentCalculator
					.getSupportedZipCodeArray())) {
				return;
			}
		}

		// here we know that either no zip is specified or zips are specified
		// and they overlap

		overlapCheck(currentCalculator, otherCalculator, errors);
	}

	/**
	 * Only retain error messages showing which fields overlap if they all
	 * overlap. If one or more don't overlap then we don't need to show any
	 * errors.
	 * 
	 * @param currentCalculator
	 * @param otherCalculator
	 * @param errors
	 */
	private void overlapCheck(FeeCalculator currentCalculator,
			FeeCalculator otherCalculator, Errors errors) {
		Map overlapErrors = new HashMap();
		if (hasOverlap(currentCalculator, otherCalculator, overlapErrors)) {
			for (Object key : overlapErrors.keySet()) {
				errors.reject((String) key, (String) overlapErrors.get(key));
			}
		}
	}

	/**
	 * error is raised when ALL the following conditions overlap (not just one)
	 * 
	 * @return
	 */
	private boolean hasOverlap(FeeCalculator currentCalculator,
			FeeCalculator otherCalculator, Map overlapErrors) {

		boolean[] overlaps = new boolean[6];

		// price overlaps?
		overlaps[0] = overlapTestRange(currentCalculator.getMinListedPrice(),
				currentCalculator.getMaxListedPrice(), otherCalculator
						.getMinListedPrice(), otherCalculator
						.getMaxListedPrice(), overlapErrors,
				"listed.price.overlap",
				"The listed price overlaps with calculator #"
						+ otherCalculator.getSequentialId());

		// sqft overlaps?
		overlaps[1] = overlapTestRange(currentCalculator.getMinSquareFeet(),
				currentCalculator.getMaxSquareFeet(), otherCalculator
						.getMinSquareFeet(),
				otherCalculator.getMaxSquareFeet(), overlapErrors,
				"square.feet.overlap",
				"The square fottage overlaps with calculator #"
						+ otherCalculator.getSequentialId());

		// propertyType overlaps?
		if (currentCalculator.getPropertyType() == PropertyType.Unspecified
				|| otherCalculator.getPropertyType() == PropertyType.Unspecified) {
			overlaps[2] = true;
			overlapErrors.put("property.type.overlap",
					"The dwelling type overlaps with calculator #"
							+ otherCalculator.getSequentialId());
		} else {
			if (currentCalculator.getPropertyType() == otherCalculator
					.getPropertyType()) {
				overlaps[2] = true;
				overlapErrors.put("property.type.overlap",
						"The property type overlaps with calculator #"
								+ otherCalculator.getSequentialId());
			}
		}

		// foundationType overlaps?
		if (currentCalculator.getFoundationType() == FoundationType.Unspecified
				|| otherCalculator.getFoundationType() == FoundationType.Unspecified) {
			overlaps[3] = true;
			overlapErrors.put("foundation.type.overlap",
					"The foundation type overlaps with calculator #"
							+ otherCalculator.getSequentialId());
		} else {
			if (currentCalculator.getFoundationType() == otherCalculator
					.getFoundationType()) {
				overlaps[3] = true;
				overlapErrors.put("foundation.type.overlap",
						"The foundation type overlaps with calculator #"
								+ otherCalculator.getSequentialId());
			}
		}

		// bathrooms overlap?
		overlaps[4] = overlapTestRange(currentCalculator
				.getMinNumberOfBathrooms(), currentCalculator
				.getMaxNumberOfBathrooms(), otherCalculator
				.getMinNumberOfBathrooms(), otherCalculator
				.getMaxNumberOfBathrooms(), overlapErrors,
				"number.of.bathrooms.overlap",
				"The number of bathrooms overlap with calculator #"
						+ otherCalculator.getSequentialId());

		// bedrooms overlap?
		overlaps[5] = overlapTestRange(currentCalculator
				.getMinNumberOfBedrooms(), currentCalculator
				.getMaxNumberOfBedrooms(), otherCalculator
				.getMinNumberOfBedrooms(), otherCalculator
				.getMaxNumberOfBedrooms(), overlapErrors,
				"number.of.bedrooms.overlap",
				"The number of bedrooms overlap with calculator #"
						+ otherCalculator.getSequentialId());

		boolean allOverlap = true;
		for (int i = 0; i < overlaps.length; i++) {
			if (!overlaps[i]) {
				allOverlap = false;
			}
		}

		return allOverlap;
	}

	private boolean overlapTestRange(double min1, double max1, double min2,
			double max2, Map overlapErrors, String messageKey, String message) {
		final long HIGH_NUM = 999999999999l;
		double max1Adjusted = max1 != 0 ? max1 : HIGH_NUM;
		double max2Adjusted = max2 != 0 ? max2 : HIGH_NUM;
		if (max1Adjusted > min2 && min1 < max2Adjusted) {
			overlapErrors.put(messageKey, message);
			return true;
		}
		return false;
	}

	public void validateAgainstOverlapWithOtherCalculators(
			BaseFeeWithAdjustersCalculator currentCalculator, Errors errors) {
		validateAgainstOverlapWithOtherCalculators(currentCalculator,
				currentCalculator.otherFeeCalculatorsForCompany(), errors);
	}

	public void validateAgainstOverlapWithOtherCalculators(
			BaseFeeWithAdjustersCalculator currentCalculator,
			List<FeeCalculator> otherCalculators, Errors errors) {
		for (FeeCalculator otherCalculator : otherCalculators) {

			log.debug("Validating calculator #"
					+ currentCalculator.getSequentialId()
					+ " against existing calculator #"
					+ otherCalculator.getSequentialId() + "... (errors count: "
					+ errors.getErrorCount() + ")");

			this.validateAgainstOverlapWithEachOther(currentCalculator,
					otherCalculator, errors);

			// Debug
			log.debug("Done validating calculator #"
					+ currentCalculator.getSequentialId()
					+ " against existing calculator #"
					+ otherCalculator.getSequentialId() + ". (errors count: "
					+ errors.getErrorCount() + ")");
		}
	}

	public void validate(Object obj, Errors errors) {
		BaseFeeWithAdjustersCalculator currentCalculator = (BaseFeeWithAdjustersCalculator) obj;

		// Base fee required
		if (currentCalculator.getBaseFee() <= 0) {
			errors.reject("fee.calculator.missing.base.fee",
					"Base fee amount required.");
		}

		// Supported zips format check
		if (!StringUtils.isEmpty(currentCalculator.getSupportedZipCodes())
				&& !StringUtils.containsOnly(currentCalculator
						.getSupportedZipCodes(), "0123456789, ")) {
			errors
					.reject("fee.calculator.invalid.supported.zips",
							"Supported zip codes must contain numeric zip codes, comma, or spaces only.");
		}

		// Sq ft range check
		if (currentCalculator.getMaxSquareFeet() != 0
				&& currentCalculator.getMinSquareFeet() > currentCalculator
						.getMaxSquareFeet()) {
			errors.reject("fee.calculator.invalid.sqft.range",
					"Min square footage cannot exceed max square footage.");
		}

		// Price range check
		if (currentCalculator.getMaxListedPrice() != 0
				&& currentCalculator.getMinListedPrice() > currentCalculator
						.getMaxListedPrice()) {
			errors.reject("fee.calculator.invalid.price.range",
					"Min price cannot exceed max price.");
		}

		// Beds range check
		if (currentCalculator.getMaxNumberOfBedrooms() != 0
				&& currentCalculator.getMinNumberOfBedrooms() > currentCalculator
						.getMaxNumberOfBedrooms()) {
			errors.reject("fee.calculator.invalid.beds.range",
					"Min bedrooms cannot exceed max bedrooms.");
		}

		// Baths range check
		if (currentCalculator.getMaxNumberOfBathrooms() != 0
				&& currentCalculator.getMinNumberOfBathrooms() > currentCalculator
						.getMaxNumberOfBathrooms()) {
			errors.reject("fee.calculator.invalid.baths.range",
					"Min bathrooms cannot exceed max bathrooms.");
		}

		validateAgainstOverlapWithOtherCalculators(currentCalculator, errors);
	}

}
